﻿using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Collections.Generic;
using System.IO;
using System.Diagnostics;

using JavaSubstitutes;
using Sonar;

namespace Sprites
{
	//this class basically coordinates the typical game scene with mario and enemies as opposed to the Map scene, for example
	public class LevelScene : Scene, SpriteContext
	{
		private List<Sprite> sprites = new List<Sprite>();
		private List<Sprite> spritesToAdd = new List<Sprite>();
		private List<Sprite> spritesToRemove = new List<Sprite>();
		private List<Fireball> fireballsToCheck = new List<Fireball>();
		public int fireballsOnScreen = 0;
		private List<Shell> shellsToCheck = new List<Shell>();

		private DecimalFormat df = new DecimalFormat("00");
		private DecimalFormat df2 = new DecimalFormat("000");

		public Level level;
		public Mario mario;
		protected double xCam, yCam;
		public static Image tmpImage;
		private int tickCount;

		private LevelRenderer layer;
		private BgRenderer[] bgLayer = new BgRenderer[2];

		private GraphicsConfiguration graphicsConfiguration;

		public bool paused = false;
		public int startTime = 0;
		private int timeLeft;

		//private long levelSeed;
		private int levelSeed;
		private MarioComponent renderer;
		private int levelType;
		private int levelDifficulty;

		public LevelScene(GraphicsConfiguration graphicsConfiguration, MarioComponent renderer, int seed, int levelDifficulty, int type)
		{
			this.graphicsConfiguration = graphicsConfiguration;
			this.levelSeed = seed;
			this.renderer = renderer;
			this.levelDifficulty = levelDifficulty;
			this.levelType = type;
		}


		public override void init()
		{
			level = LevelGenerator.createLevel(320, 15, levelSeed, levelDifficulty, levelType);

			if (levelType == LevelGenerator.TYPE_OVERGROUND)
				Art.startMusic(1);
			else if (levelType == LevelGenerator.TYPE_UNDERGROUND)
				Art.startMusic(2);
			else if (levelType == LevelGenerator.TYPE_CASTLE)
				Art.startMusic(3);

			paused = false;
			Sprite.spriteContext = this;
			sprites.clear();
			layer = new LevelRenderer(level, graphicsConfiguration, 320, 240);
			SetupBackgroundLayers();
			mario = new Mario(this);
			sprites.add(mario);
			startTime = 1;

			timeLeft = 200 * 15;

			tickCount = 0;
		}

		private void SetupBackgroundLayers()
		{
			for (int i = 0; i < 2; i++)
			{
				int scrollSpeed = 4 >> i;
				int w = ((level.width * 16) - 320) / scrollSpeed + 320;
				int h = ((level.height * 16) - 240) / scrollSpeed + 240;
				Level bgLevel = BgLevelGenerator.createLevel(w / 32 + 1, h / 32 + 1, i == 0, levelType);
				bgLayer[i] = new BgRenderer(bgLevel, graphicsConfiguration, 320, 240, scrollSpeed);
			}
		}

		public void checkShellCollide(Shell shell)
		{
			shellsToCheck.add(shell);
		}

		public void checkFireballCollide(Fireball fireball)
		{
			fireballsToCheck.add(fireball);
		}

		public override void tick()
		{
			timeLeft--;
			if (timeLeft == 0)
			{
				mario.die();
			}
			//xCamO = xCam;
			//yCamO = yCam;

			if (startTime > 0)
			{
				startTime++;
			}
			
			xCam = mario.x - 160; //meliot: put the camera in the middle of the scene? 320/2=160		
			//Debug.WriteLine("xCam now: " + xCam.ToString());

			if (xCam < 0) xCam = 0; //don't let the camera go negative
			if (xCam > level.width * 16 - 320) xCam = level.width * 16 - 320; //don't let it past 4800 pixels long, note: 320x240 dependency

			fireballsOnScreen = 0;			
			HandleOffscreenSpritesAndFireballs();

			//todo: refactor to methods...
			if (paused)
			{
				UpdateDuringPause();
			}
			else
			{
				//todo: refactor to method
				tickCount++;
				level.tick();

				//bool hasShotCannon = false;
				int xCannon = 0;

				for (int x = (int)xCam / 16 - 1; x <= (int)(xCam + layer.Width) / 16 + 1; x++)
					for (int y = (int)yCam / 16 - 1; y <= (int)(yCam + layer.Height) / 16 + 1; y++)
					{
						int dir = 0;

						if (x * 16 + 8 > mario.x + 16) dir = -1;
						if (x * 16 + 8 < mario.x - 16) dir = 1;

						SpawnEnemies(x, y, dir, level.getSpriteTemplate(x, y)); //meliot- I'm guessing this is the function. If it's not precisely what this does, then it's similar
 
						if (dir != 0)
						{
							//does it do anything OTHER than shoot the cannon?
							AnimateCannon(ref xCannon, x, y, dir, level.getBlock(x, y));
						}
					}

				sprites.ForEach(sprite => sprite.tick());
				sprites.ForEach(sprite => sprite.collideCheck());

				CheckShells();
				CheckFireBalls();
			}

			sprites.addAll(0, spritesToAdd);
			spritesToAdd.clear();

			sprites.removeAll(spritesToRemove);			
			spritesToRemove.clear();
		}

		//meliot: does it do anything OTHER than shoot the cannon?
		private void AnimateCannon(ref int xCannon, int x, int y, int dir, byte b)
		{
			bool shotCannon=false;
			if (((Level.TILE_BEHAVIORS[b & 0xff]) & Level.BIT_ANIMATED) > 0)
			{
				if ((b % 16) / 4 == 3 && b / 16 == 0)
				{
					if ((tickCount - x * 2) % 100 == 0)
					{
						xCannon = x;
						for (int i = 0; i < 8; i++)
						{
							addSprite(new Sparkle(x * 16 + 8, y * 16 + (int)(RandomExtensions.random() * 16), (double)RandomExtensions.random() * dir, 0, 0, 1, 5));
						}
						addSprite(new BulletBill(this, x * 16 + 8 + dir * 8, y * 16 + 15, dir));
						shotCannon = true;
					}
				}
			}			
			if(shotCannon)sound.play(Art.samples[Art.SAMPLE_CANNON_FIRE], new FixedSoundSource(xCannon * 16, yCam + 120), 1, 1, 1);
		}

		//meliot- I'm guessing this is the function. If it's not precisely what this does, then it's similar
		private void SpawnEnemies(int x, int y, int dir, SpriteTemplate spriteTemplate)
		{

			if (spriteTemplate != null)
			{
				if (spriteTemplate.lastVisibleTick != tickCount - 1)
				{
					if (spriteTemplate.sprite == null || !(sprites.IndexOf(spriteTemplate.sprite) != -1))
					{
						spriteTemplate.spawn(this, x, y, dir);
					}
				}
				spriteTemplate.lastVisibleTick = tickCount;
			}
		}

		private void HandleOffscreenSpritesAndFireballs()
		{
			sprites.ForEach(sprite =>
			{
				if (sprite == mario) return;

				double xd = sprite.x - xCam; //meliot: distance from mario's target coords?
				double yd = sprite.y - yCam;
				if (xd < -64 || xd > 320 + 64 || yd < -64 || yd > 240 + 64)
				{
					//meliot: if it's too far off screen, remove it?
					removeSprite(sprite);
				}
				else
				{
					if (sprite is Fireball) fireballsOnScreen++;
				}
			}
			);
		}

		private void UpdateDuringPause()
		{
			foreach (Sprite sprite in sprites)
			{
				if (sprite == mario)
				{
					sprite.tick();
				}
				else
				{
					sprite.tickNoMove();
				}
			}
		}

		private void CheckShells()
		{
			foreach (Shell shell in shellsToCheck)
			{
				foreach (Sprite sprite in sprites)
				{
					//todo: convert to single if
					if (sprite != shell && !shell.dead)
					{
						if (sprite.shellCollideCheck(shell))
						{
							if (mario.carried == shell && !shell.dead)
							{
								mario.carried = null;
								shell.die();
							}
						}
					}
				}
			}
			shellsToCheck.clear();
		}

		private void CheckFireBalls()
		{
			foreach (Fireball fireball in fireballsToCheck)
			{
				foreach (Sprite sprite in sprites)
				{
					//todo: convert to single if
					if (sprite != fireball && !fireball.dead)
					{
						if (sprite.fireballCollideCheck(fireball))
						{
							fireball.die();
						}
					}
				}
			}
			fireballsToCheck.clear();
		}

		public override void render(WriteableBitmap wbmp, double alpha)
		{
			//meliot: xCam and yCam are the camera for this level- if the background isn't tracking with mario, this will be why!
			int xCam = (int)(mario.xOld + (mario.x - mario.xOld) * alpha) - 160;
			int yCam = (int)(mario.yOld + (mario.y - mario.yOld) * alpha) - 120;

			if (xCam % 2 == 0) Debug.WriteLine("cam x/y:" + xCam.ToString() + ", " + yCam.ToString());

			HandleCameraOffScreen(ref xCam, ref yCam);
			RenderBackgrounds(wbmp, alpha, xCam, yCam);

			//wbmp.translate(-xCam, -yCam); //meliot- what was the purpose of this in the original code- was everything drawn backwards?
			foreach (Sprite sprite in sprites)
			{
				//if (sprite.layer == 0) sprite.render(wbmp, alpha);
				if (sprite.layer == 0) sprite.render(wbmp, alpha); //only layer 0?				
			}
			//wbmp.translate(xCam, yCam);

			layer.setCam(wbmp, xCam, yCam);
			layer.render(wbmp, tickCount, paused ? 0 : alpha); //meliot: if we're paused- DON'T RENDER IT?
			layer.renderExit0(wbmp, tickCount, paused ? 0 : alpha, mario.winTime == 0); //meliot: if we're paused- DON'T RENDER IT?

			wbmp.translate(-xCam, -yCam);
			foreach (Sprite sprite in sprites)
			{
				//wbmp.Invalidate();
				if (sprite.layer == 1) sprite.render(wbmp, alpha);
					
				//wbmp.Invalidate();
			}
			wbmp.translate(xCam, yCam);
			wbmp.setColor(Colors.Black);
			layer.renderExit1(wbmp, tickCount, paused ? 0 : alpha);

			int time = (timeLeft + 15 - 1) / 15;
			if (time < 0) time = 0;

			RenderText(wbmp, time);

			if (startTime > 0)
			{
				double t = startTime + alpha - 2;
				t = t * t * 0.6f;
				renderBlackout(wbmp, 160, 120, (int)(t));
			}

			if (mario.winTime > 0)
			{
				double t = mario.winTime + alpha;
				t = t * t * 0.2f;

				if (t > 900)
				{
					renderer.ShowLevelWonScene();
				}

				renderBlackout(wbmp, (int)(mario.xDeathPos - xCam), (int)(mario.yDeathPos - yCam), (int)(320 - t));
			}

			if (mario.deathTime > 0)
			{
				double t = mario.deathTime + alpha;
				t = t * t * 0.4f;

				if (t > 1800)
				{
					renderer.ShowLevelFailedScene();
				}

				renderBlackout(wbmp, (int)(mario.xDeathPos - xCam), (int)(mario.yDeathPos - yCam), (int)(320 - t));
			}
		}

		private void HandleCameraOffScreen(ref int xCam, ref int yCam)
		{
			if (xCam < 0) xCam = 0;
			if (yCam < 0) yCam = 0;
			if (xCam > level.width * 16 - 320) xCam = level.width * 16 - 320;
			if (yCam > level.height * 16 - 240) yCam = level.height * 16 - 240;
		}

		private void RenderBackgrounds(WriteableBitmap wbmp, double alpha, int xCam, int yCam)
		{
			//meliot testing removeme- it you take out "NOTRENDERBACKGROUND" from conditional compilation on this asm, it'll render the background
#if !NOTRENDERBACKGROUND
			for (int i = 0; i < 2; i++)
			{
				bgLayer[i].setCam(xCam, yCam);
				bgLayer[i].render(wbmp, tickCount, alpha);
			}
#endif
		}

		private void RenderText(WriteableBitmap wbmp, int time)
		{
			drawStringWithDropShadow(wbmp, "MARIO " + df.format(Mario.lives), 0, 0, 7);
			drawStringWithDropShadow(wbmp, "00000000", 0, 1, 7);

			drawStringWithDropShadow(wbmp, "COIN", 14, 0, 7);
			drawStringWithDropShadow(wbmp, " " + df.format(Mario.coins), 14, 1, 7);

			drawStringWithDropShadow(wbmp, "WORLD", 24, 0, 7);
			drawStringWithDropShadow(wbmp, " " + Mario.levelString, 24, 1, 7);

			drawStringWithDropShadow(wbmp, "TIME", 35, 0, 7);
			drawStringWithDropShadow(wbmp, " " + df2.format(time), 35, 1, 7);
		}

		private void drawStringWithDropShadow(WriteableBitmap wbmp, String text, int x, int y, int c)
		{
			drawString(wbmp, text, x * 8 + 5, y * 8 + 5, 0);
			drawString(wbmp, text, x * 8 + 4, y * 8 + 4, c);
		}

		private void drawString(WriteableBitmap wbmp, String text, int x, int y, int c)
		{
			char[] ch = text.toCharArray();
			for (int i = 0; i < ch.Length; i++)
			{
				wbmp.drawImage(Art.font[ch[i] - 32, c], x + i * 8, y);
			}
		}

		public static void renderBlackout(WriteableBitmap wbmp, int x, int y, int radius)
		{
			//TODO: the way this is SUPPOSED to work is that it starts with a black screen on top of 
			//the actual screen, then it gradually expands a circular area of the actual screen until
			//there's no more black. So it's like a spotlight on a blackened stage that gradually expands.
			//See example: http://www.youtube.com/watch?v=hRCmd6ArCfs&feature=related

			//Consider: opacity mask
			if (radius > 320 || radius <= 0) return;

			WriteableBitmap blackScreen = new WriteableBitmap(wbmp.PixelWidth, wbmp.PixelHeight);
			blackScreen.FillRectangle(0, 0, wbmp.PixelWidth, wbmp.PixelHeight, Colors.Black); //black out the new bmp
			blackScreen.FillEllipseCentered(x, y, radius / 2, radius / 2, Colors.Transparent); //create a circular transparent area

			//draw the result onto the screen
			wbmp.drawImage(blackScreen, 0, 0);
		}

		public void addSprite(Sprite sprite)
		{
			spritesToAdd.add(sprite);
			sprite.tick();
		}

		public void removeSprite(Sprite sprite)
		{
			spritesToRemove.add(sprite);
		}

		public double getX(double alpha)
		{
			int xCam = (int)(mario.xOld + (mario.x - mario.xOld) * alpha) - 160;

			if (xCam < 0) xCam = 0;

			return xCam + 160;
		}

		public double getY(double alpha)
		{
			return 0; //meliot: ? was this the original code
		}

		public void bump(int x, int y, bool canBreakBricks)
		{
			byte block = level.getBlock(x, y);

			if ((Level.TILE_BEHAVIORS[block & 0xff] & Level.BIT_BUMPABLE) > 0)
			{
				bumpInto(x, y - 1);
				level.setBlock(x, y, (byte)4);
				level.setBlockData(x, y, (byte)4);

				if (((Level.TILE_BEHAVIORS[block & 0xff]) & Level.BIT_SPECIAL) > 0)
				{
					sound.play(Art.samples[Art.SAMPLE_ITEM_SPROUT], new FixedSoundSource(x * 16 + 8, y * 16 + 8), 1, 1, 1);
					if (!Mario.large)
					{
						addSprite(new Mushroom(this, x * 16 + 8, y * 16 + 8));
					}
					else
					{
						addSprite(new FireFlower(this, x * 16 + 8, y * 16 + 8));
					}
				}
				else
				{
					Mario.getCoin();
					sound.play(Art.samples[Art.SAMPLE_GET_COIN], new FixedSoundSource(x * 16 + 8, y * 16 + 8), 1, 1, 1);
					addSprite(new CoinAnim(x, y));
				}
			}

			if ((Level.TILE_BEHAVIORS[block & 0xff] & Level.BIT_BREAKABLE) > 0)
			{
				bumpInto(x, y - 1);
				if (canBreakBricks)
				{
					sound.play(Art.samples[Art.SAMPLE_BREAK_BLOCK], new FixedSoundSource(x * 16 + 8, y * 16 + 8), 1, 1, 1);
					level.setBlock(x, y, (byte)0);
					for (int xx = 0; xx < 2; xx++)
						for (int yy = 0; yy < 2; yy++)
							addSprite(new Particle(x * 16 + xx * 8 + 4, y * 16 + yy * 8 + 4, (xx * 2 - 1) * 4, (yy * 2 - 1) * 4 - 8));
				}
				else
				{
					level.setBlockData(x, y, (byte)4);
				}
			}
		}

		public void bumpInto(int x, int y)
		{
			byte block = level.getBlock(x, y);
			if (((Level.TILE_BEHAVIORS[block & 0xff]) & Level.BIT_PICKUPABLE) > 0)
			{
				Mario.getCoin();
				sound.play(Art.samples[Art.SAMPLE_GET_COIN], new FixedSoundSource(x * 16 + 8, y * 16 + 8), 1, 1, 1);
				level.setBlock(x, y, (byte)0);
				addSprite(new CoinAnim(x, y + 1));
			}

			foreach (Sprite sprite in sprites)
			{
				sprite.bumpCheck(x, y);
			}
		}
	}
}
